﻿using System;
using System.Diagnostics;
using Microsoft.CSharp;
using System.CodeDom.Compiler;
using System.IO;
using System.Threading;
using System.Security.Principal;
using System.Security;
using System.Security.Permissions;

namespace LabChecker.Models.Programs
{
    public class CSharpProgram : Program
    {
        private bool _wasCompiled;
        private string _assemblyPath;
        private string _tempDir;
        private string _inputFileName;
        private string _outputFileName;
        private Exception _threadEx;
        private AppDomain _domain;

        public CSharpProgram(string code) : base(code) { }        

        public override CompilationResultBase Compile()
        {
            _wasCompiled = true;
            return GetCompileResult(this.Code);            
        }

        private void Prepare()
        {
            string preparedCode = AddInOutRedirect(this.Code);

            if (!(GetCompileResult(preparedCode) is CompilationResult.Success))
            {
                throw new Exception("Doesn't compile");
            }
        }

        private CompilationResultBase GetCompileResult(string code)
        {
            CSharpCodeProvider prov = new CSharpCodeProvider();
            CompilerParameters pars = new CompilerParameters();
            pars.CompilerOptions = "/t:winexe /d:check";
            pars.IncludeDebugInformation = true;
            pars.GenerateExecutable = true;

            var compilerResults = prov.CompileAssemblyFromSource(pars, code);

            foreach (CompilerError compilationError in compilerResults.Errors)
            {
                if (compilationError.IsWarning == false)
                {
                    return new CompilationResult.Error(
                        compilationError.ErrorText,
                        compilationError.Line,
                        compilationError.Column);
                }
            }

            _assemblyPath = compilerResults.PathToAssembly;

            return new CompilationResult.Success();
        }

        public override string Run(string input)
        {
            if (!_wasCompiled)
            {
                throw new InvalidOperationException("First compile program!");
            }

            var tempDir = FileSystemService.CreateTempDirectory();
            _tempDir = tempDir;
            _inputFileName = Path.Combine(tempDir, "input.txt");
            _outputFileName = Path.Combine(tempDir, "output.txt");

            FileSystemService.CreateFileFromText(_inputFileName, input);

            if (Code.Contains("DllImport"))
            {
                throw new Exceptions.InvalidAction();
            }

            Prepare();

            var th = new Thread(new ParameterizedThreadStart(RunThread));
            th.Priority = ThreadPriority.Lowest;
            IntPtr token = WindowsIdentity.GetCurrent().Token;

            try
            {
                th.Start(token);
                var time = 300;
                th.Join(new TimeSpan(0, 0, 0, 0, time));

                if (th.ThreadState == System.Threading.ThreadState.Running)
                {
                    AppDomain.Unload(_domain);
                    _domain = null;
                    th.Abort();
                    throw new Exceptions.TimeLimitExceeded(3);
                }
                if (_threadEx != null)
                {
                    if (_threadEx is SecurityException)
                    {
                        throw new Exceptions.InvalidAction();
                    }
                    var runtimeEx = new Exceptions.RuntimeError(_threadEx);
                    if (_threadEx is OutOfMemoryException)
                    {
                        GC.Collect();
                    }
                    throw runtimeEx;
                }
                return FileSystemService.GetFileContents(_outputFileName);
            }
            finally
            {
                if (_domain != null)
                {
                    AppDomain.Unload(_domain);
                    _domain = null;
                }
            }
        }

        private string AddInOutRedirect(string sourceCode)
        {
            var result = sourceCode;
            var mainPos = sourceCode.IndexOf("Main(");
            var parenthPos = mainPos;

            while (sourceCode[parenthPos] != '{')
            {
                parenthPos++;
            }

            //StreamReader __input = new StreamReader("input.txt"); StreamWriter __output = new StreamWriter("output.txt"); Console.SetIn(__input); Console.SetOut(__output);
            //__input.Close(); __output.Close();

            string consoleRedirectCode =
                "System.IO.StreamReader __input = new System.IO.StreamReader(@\"" +
                _inputFileName +
                "\"); System.IO.StreamWriter __output = new System.IO.StreamWriter(@\"" +
                _outputFileName +
                "\"); Console.SetIn(__input); Console.SetOut(__output); try {";                

            result = result.Insert(parenthPos + 1, consoleRedirectCode);

            var parentDiff = 1;
            var currPos = parenthPos + 1;
            var hitOurParenth = false;
            do
            {
                if (result[currPos] == '{')
                {
                    if (!hitOurParenth)
                        hitOurParenth = true;
                    else
                        parentDiff++;
                }
                else if (result[currPos] == '}')
                    parentDiff--;
                currPos++;
            } while (parentDiff > 0);

            result = result.Insert(currPos - 1, "} finally { __input.Close(); __output.Close(); }");

            return result;
        }

        private void RunThread(object token)
        {
            WindowsIdentity.Impersonate((IntPtr)token);
            if (_domain == null)
            {
                _domain = CreateNewDomain();
            }
            try
            {
                _domain.ExecuteAssemblyByName(Path.GetFileNameWithoutExtension(_assemblyPath));
            }
            catch (Exception ex)
            {
                _threadEx = ex;
            }
        }

        private AppDomain CreateNewDomain()
        {
            AppDomainSetup adSetup = new AppDomainSetup();
            adSetup.ApplicationBase = Path.GetTempPath();

            var permSet = new PermissionSet(PermissionState.None);
            permSet.AddPermission(new SecurityPermission(SecurityPermissionFlag.Execution | SecurityPermissionFlag.UnmanagedCode));
            permSet.AddPermission(new UIPermission(PermissionState.Unrestricted));
            permSet.AddPermission(new FileIOPermission(FileIOPermissionAccess.Read, _inputFileName));
            permSet.AddPermission(new FileIOPermission(FileIOPermissionAccess.Write | FileIOPermissionAccess.Append, _outputFileName));
            
            AppDomain newDomain = AppDomain.CreateDomain(Guid.NewGuid().ToString(), null, adSetup, permSet);
            return newDomain;
        }

        public override void CleanUp()
        {
            FileSystemService.DeleteDirectory(_tempDir);
        }
    }
}